--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 2dcc759c88f3f2471ffcf6146721200a16aadede
Parents : 8fe8ef4
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Signature : T66BB85Valid, signed by author
Date : 2026-07-15T02:39:59+02:00
Render connection telemetry points on map
Changes
4 files changed, 85 insertions(+), 8 deletions(-)
Diff
diff --git a/sbapp/main.py b/sbapp/main.py
index 839dbfef..cb58b502 100644
--- a/sbapp/main.py
+++ b/sbapp/main.py
@@ -169,7 +169,7 @@ def apply_ui_scale():
# until a Kivy release is made that actually works.
#
if sys.version_info[0] >= 3 and sys.version_info[1] >= 14:
- if RNS.vendor.platformutils.is_linux():
+ if RNS.vendor.platformutils.is_linux() and not args.daemon:
from .compat import python314_kivy_patch
python314_kivy_patch()
@@ -1809,6 +1809,14 @@ class SidebandApp(MDApp):
self.map_view.layers_action()
else:
self.announces_action(self)
+
+ if text == "c":
+ if self.root.ids.screen_manager.current == "map_screen":
+ self.map_view.connection_maps_action()
+
+ if text == "x":
+ if self.root.ids.screen_manager.current == "map_screen":
+ self.map_view.interfaces_action()
if text == "m":
if self.root.ids.screen_manager.current == "messages_screen":
@@ -5047,11 +5055,13 @@ class SidebandApp(MDApp):
elif iface_type == "BackboneInterface": return "wan"
elif iface_type == "TCPServerInterface": return "lan-connect"
elif iface_type == "I2PInterface": return "lock-pattern"
+ elif iface_type == "connection": return "cellphone-link"
else: return "lan-connect"
def iface_appearance(self, iface_type):
- f = 0.8
- return [self.iface_icon(iface_type), [255/255, 253/255, 231/255, 1], [75/255*f, 97/255*f, 108/255*f,1]]
+ f = 0.95 if iface_type == "connection" else 0.8
+ a = 1.0 if iface_type == "connection" else 1.0
+ return [self.iface_icon(iface_type), [255/255, 253/255, 231/255, a], [75/255*f, 97/255*f, 108/255*f, a]]
### Object details screen
######################################
diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py
index f76c0644..a33b6717 100644
--- a/sbapp/sideband/core.py
+++ b/sbapp/sideband/core.py
@@ -807,6 +807,7 @@ class SidebandCore():
if not "map_layer" in self.config: self.config["map_layer"] = None
if not "map_cluster" in self.config: self.config["map_cluster"] = True
if not "map_interfaces" in self.config: self.config["map_interfaces"] = True
+ if not "map_connection_maps" in self.config: self.config["map_connection_maps"] = False
if not "discover_interfaces" in self.config: self.config["discover_interfaces"] = True
if not "map_storage_path" in self.config: self.config["map_storage_path"] = None
diff --git a/sbapp/ui/map.py b/sbapp/ui/map.py
index 26be4f27..71d166c4 100644
--- a/sbapp/ui/map.py
+++ b/sbapp/ui/map.py
@@ -48,6 +48,7 @@ class Map():
self.clustered_layer = None
self.clustered_markers = {}
self.interface_discovery_hashes = {}
+ self.connection_map_hashes = {}
self.last_interfaces_update = 0
if not self.app.root.ids.screen_manager.has_screen("map_screen"):
@@ -239,6 +240,11 @@ class Map():
if self.app.sideband.config["map_interfaces"]: self.app.sideband.config["map_cluster"] = True
self.update_markers()
+ def connection_maps_action(self, sender=None):
+ self.app.sideband.config["map_connection_maps"] ^= True
+ if self.app.sideband.config["map_connection_maps"]: self.app.sideband.config["map_cluster"] = True
+ self.update_markers()
+
def layers_action(self, sender=None):
try:
ml = self.map_layer
@@ -331,6 +337,7 @@ class Map():
self.map_settings_screen.ids.map_storage_external.active = self.app.sideband.config["map_storage_external"]
self.map_settings_screen.ids.map_cluster.active = self.app.sideband.config["map_cluster"]
self.map_settings_screen.ids.map_interfaces.active = self.app.sideband.config["map_interfaces"]
+ self.map_settings_screen.ids.map_connection_maps.active = self.app.sideband.config["map_connection_maps"]
def settings_init(self):
self.settings_load_states()
@@ -340,6 +347,7 @@ class Map():
self.app.sideband.config["map_use_offline"] = self.map_settings_screen.ids.map_use_offline.active
self.app.sideband.config["map_cluster"] = self.map_settings_screen.ids.map_cluster.active
self.app.sideband.config["map_interfaces"] = self.map_settings_screen.ids.map_interfaces.active
+ self.app.sideband.config["map_connection_maps"] = self.map_settings_screen.ids.map_connection_maps.active
self.app.sideband.save_configuration()
def external_toggle(sender=None, event=None):
@@ -362,6 +370,7 @@ class Map():
self.map_settings_screen.ids.map_storage_external.bind(active=external_toggle)
self.map_settings_screen.ids.map_cluster.bind(active=map_settings_save)
self.map_settings_screen.ids.map_interfaces.bind(active=map_settings_save)
+ self.map_settings_screen.ids.map_connection_maps.bind(active=map_settings_save)
def settings_action(self, sender=None, direction="left"):
if not self.app.root.ids.screen_manager.has_screen("map_settings_screen"):
@@ -463,6 +472,9 @@ class Map():
own_address = self.app.sideband.lxmf_destination.hash
changes = False
+ if not self.app.sideband.config["map_connection_maps"]:
+ self.connection_map_hashes = {}
+
if not self.app.sideband.config["map_interfaces"]:
self.interface_discovery_hashes = {}
self.last_interfaces_update = 0
@@ -533,7 +545,7 @@ class Map():
try:
stale_markers = []
for marker in self.clustered_markers:
- if not marker in telemetry_entries and not marker in self.interface_discovery_hashes:
+ if not marker in telemetry_entries and not marker in self.interface_discovery_hashes and not marker in self.connection_map_hashes:
if marker == own_address:
if not retain_own: stale_markers.append(marker)
else: stale_markers.append(marker)
@@ -569,10 +581,6 @@ class Map():
if "location" in telemetry and telemetry["location"] != None and telemetry["location"]["latitude"] != None and telemetry["location"]["longitude"] != None:
latest_viewable = telemetry
break
-
- # TODO: Telemetry entries with connection map sensor types are skipped for now,
- # until a proper rendering mechanism is implemented
- elif "connection_map" in telemetry: break
if latest_viewable != None:
l = latest_viewable["location"]
@@ -596,6 +604,40 @@ class Map():
clustered_marker.update_props()
changes = True
+ if self.app.sideband.config["map_connection_maps"]:
+ RNS.log(f"RENDERING CMAP")
+ latest_connection_maps = None
+ for telemetry_entry in sorted(telemetry_entries[telemetry_source], key=lambda t: t[0], reverse=True):
+ telemetry_timestamp = telemetry_entry[0]
+ telemetry_data = telemetry_entry[1]
+ t = Telemeter.from_packed(telemetry_data)
+ if t != None:
+ telemetry = t.read_all()
+ if "connection_map" in telemetry and telemetry["connection_map"] != None and "maps" in telemetry["connection_map"]:
+ latest_connection_maps = telemetry
+ RNS.log("FOUND DATA")
+ break
+
+ if latest_connection_maps:
+ m = latest_connection_maps["connection_map"]
+ for mn in m["maps"]:
+ RNS.log(f"PROCESSING MAP {mn}")
+ cm = m["maps"][mn]
+ if not "points" in cm: continue
+ for p_hash in cm["points"]:
+ p = cm["points"][p_hash]
+ if "latitude" in p and "longitude" in p and "altitude" in p:
+ self.connection_map_hashes[p_hash] = p
+ if not p_hash in self.clustered_markers:
+ if_appearance = self.app.iface_appearance("connection")
+ plat = float(p["latitude"]); plon = float(p["longitude"]); palt = float(p["altitude"])
+ telemetry = {"location": {"latitude": plat, "longitude": plon, "altitude": palt, "last_update": time.time()}}
+ marker = self.create_clustered_marker(p_hash, telemetry, if_appearance)
+ marker.parent = telemetry_source
+ if marker != None:
+ self.clustered_markers[p_hash] = marker
+ changes = True
+
except Exception as e:
RNS.log("Error while updating clustered map entry for "+RNS.prettyhexrep(telemetry_source)+": "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
@@ -838,6 +880,21 @@ MDScreen:
pos_hint: {"center_y": 0.3}
active: False
+ MDBoxLayout:
+ orientation: "horizontal"
+ padding: [0,0,dp(24),0]
+ size_hint_y: None
+ height: dp(48)
+
+ MDLabel:
+ text: "Connection Telemetry"
+ font_style: "H6"
+
+ MDSwitch:
+ id: map_connection_maps
+ pos_hint: {"center_y": 0.3}
+ active: False
+
MDBoxLayout:
orientation: "horizontal"
padding: [0,0,dp(24),0]
diff --git a/sbapp/ui/objectdetails.py b/sbapp/ui/objectdetails.py
index 441f0c43..5b18f758 100644
--- a/sbapp/ui/objectdetails.py
+++ b/sbapp/ui/objectdetails.py
@@ -822,6 +822,15 @@ class RVDetails(MDRecycleView):
te = "enabled" if s["values"]["transport_enabled"] else "disabled"
formatted_values = f"Reticulum Transport [b]{te}[/b]"
+ elif name == "Connection Map":
+ mc = 0; pc = 0
+ if "maps" in s["values"]:
+ for mn in s["values"]["maps"]:
+ mc += 1
+ pc += len(s["values"]["maps"][mn]["points"])
+ ms = "s" if mc != 1 else ""; ps = "s" if pc != 1 else ""
+ formatted_values = f"[b]{mc}[/b] connection map{ms}, [b]{pc}[/b] point{ps}"
+
elif name == "LXMF Propagation":
tp = str(s["values"]["total_peers"])
ap = str(s["values"]["active_peers"])
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────